package kernel.process;

import java.io.File;
import java.io.PrintWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

import kernel.FileManager;
import kernel.IOManager;
import kernel.ProcessMemory;
import kernel.exception.OSException;

/**
 * Class responsible for running of the whole input string of commands (separated by pipes).
 * @author Petr Hajek, Jaromir Lepic
 */
public class ProcessDispatcher extends Thread {
	
	private long id;
	private AtomicLong actualPID;
	private StringBuffer name;
	private List<AbstractProcess> programs;
	private AtomicLong startingTime;
	private boolean silent = false;
	
	public ProcessDispatcher(List<AbstractProcess> prgs, long id) {
		this.actualPID = new AtomicLong();
		this.programs = prgs;
		this.id = id;
	}
	
	public ProcessDispatcher setSilent(boolean silent){
		this.silent = silent;
		return this;
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void run() {
		this.startingTime = new AtomicLong(System.currentTimeMillis());
		String out = "";
		for (int i = 0; i < this.programs.size(); i++) {
			try {
				this.setActualPID(this.programs.get(i).getPid());
				this.setActualName(this.programs.get(i).name);
				//out = this.programs.get(i).doJobManager(out, this);
				out = doJobManager(out, this.programs.get(i), i < this.programs.size()-1);
			} catch (OSException e) {
				out = "";
				if (!silent)
					IOManager.getInstance().sendMessageToClient(this.programs.get(i).getShellID(), "Error: " + e.getMessage(), true, this);
				destroyPd();
				return;
			}
		}
		interrupted();
		destroyPd();
	}
	
	/**
	 * Manages all the IO operation connected with given process (such as redirection from and to a file) and runs the process "doJob" code.
	 * @param input				Name of the file from which the process should get an input.
	 * @param process			Process to run its job.
	 * @param hasNextProcess	True if the process is connected with next process by a pipe.
	 * @return					Output of the process.
	 * @throws OSException
	 */
	public String doJobManager(String input, AbstractProcess process, boolean hasNextProcess) throws OSException {
		process.alive = true;
		
		if (process.getParams().contains(AbstractProcess.HELP) || process.getParams().contains(AbstractProcess.HELP2)) {
			String help = "Man page for " + process.getName().toUpperCase() + ":\n"
					+ process.man(process.getName());
			if (process.getOutput().equals("")) {
				if (hasNextProcess)
					return help;
				else if (!silent){
					IOManager.getInstance().sendMessageToClient(this.programs.get(0).getShellID(), help, true, this);
				}
			}
			else {
				if (process.getAppend()){
					FileManager.appendData(help, absPath(process.getOutput(), process.getShellID()));
				} else {
					FileManager.writeData(help, absPath(process.getOutput(), process.getShellID()));
				}
			}
			return "";
		}
		
		if (!process.getInput().equals("")) {
			if (input.equals("")) {
				input = readInput(process.getInput(), process);
			}
		}
		else {
			if (process.needParams() && process.getParams().isEmpty() && input.equals("")) {
				input = (IOManager.getInstance().requestSTDInputText(process.shellID, this));
			}
		}
		if (process.shouldEnd){
			process.alive = false;
			if (!silent)
				IOManager.getInstance().sendMessageToClient(this.programs.get(0).getShellID(), "", true, this);
			return "";
		} else {
			PrintWriter writer = null;
			if (!process.getOutput().equals("")) {
				writer = FileManager.openWriter(absPath(process.getOutput(), process.getShellID()), process.getAppend());
			}
			
			StringBuilder outBuffer = new StringBuilder("");
			String out = process.doJob(input, this);
			if (process.getOutput().equals("")) {
				if (hasNextProcess)
					outBuffer.append(out);
				else if (!silent){
					IOManager.getInstance().sendMessageToClient(this.programs.get(0).getShellID(), out, false, null);
				}
			}
			else {
				writer.println(out);
			}
			while (!process.shouldEnd()){
				if (interrupted()) break;
				out = process.continueJob();
				if (process.getOutput().equals("") && out != null) {
					if (hasNextProcess)
						outBuffer.append(out);
					else if (!"".equals(out) && !silent)
						IOManager.getInstance().sendMessageToClient(this.programs.get(0).getShellID(), out, false, this);
				}
				else if (out != null){
					writer.println(out);
				}
			}
			
			out = process.finishJob();
			if (process.getOutput().equals("")) {
				if (hasNextProcess && out != null)
					outBuffer.append(out);
				else if (!silent) {
					if (this.id == 0) IOManager.getInstance().sendMessageToClient(this.programs.get(0).getShellID(), out, false, this);
					else IOManager.getInstance().sendMessageToClient(this.programs.get(0).getShellID(), out, true, this);
				}
			}
			else {
				if (out != null) {
					writer.println(out);
				}
				if (!silent)
					IOManager.getInstance().sendMessageToClient(this.programs.get(0).getShellID(), "", true, this);
			}
			if (writer != null) {
				writer.close();
			}
			
			while (!process.shouldEnd)
				Thread.yield();
			return outBuffer.toString();
		}
	}
	
	/**
	 * Reads a given file.
	 * @param in		Name of the file.
	 * @param process	Process which wants to read an input from a file.
	 * @return			File input.
	 * @throws OSException
	 */
	public String readInput(String in, AbstractProcess process) throws OSException {
		return FileManager.readData(FileManager.getPath(in, getWd(process.getShellID())));
	}
	
	/**
	 * Returns absolute path from the relative one.
	 * @param path		Relative path. (If absolute path is given, no changes happen)
	 * @param shellID	ID of the actual shell. 
	 * @return			Absolute path.
	 * @throws OSException
	 */
	public File absPath(String path, long shellID) throws OSException {
		return FileManager.getPath(path, getWd(shellID));
	}
	
	/**
	 * Returns a working directory for given shell.
	 * @param shellID	ID of a shell.
	 * @return			Working directory.
	 */
	public File getWd(long shellID) {
		return IOManager.getInstance().getWorkingDirectory(shellID);
	}
	
	/**
	 * Deletes this process dispatcher from process memory.
	 */
	private void destroyPd() {
		try {
			if (this.id != 0) ProcessMemory.putIntoOldPQ(this.id);
		} catch (InterruptedException e) {
		}
	}
	
	/**
	 * Gets start time of this process dispatcher.
	 * @return Start time of this process dispatcher.
	 */
	public String getStartTime() {
		Calendar cal = Calendar.getInstance();
		cal.setTimeInMillis(this.startingTime.get());
		DateFormat dt = new SimpleDateFormat("HH:mm:ss");
		Date date = cal.getTime();
		return dt.format(date);
	}
	
	/**
	 * Gets all running processes in this process dispatcher.
	 * @return	All running processes stored in this process dispatcher.
	 */
	public StringBuilder getRunningProgramsInDisp() {
		boolean running = false;
		StringBuilder str = new StringBuilder(); 
		for (int i = 0; i < this.programs.size(); i++) {
			long pid = this.programs.get(i).getPid();
			if (pid == this.getActualPID()) {
				running = true;
			}
			if (running) {
				str.append(this.programs.get(i).getUserID() + "\t" + pid + "\t" + "tty" + this.programs.get(i).shellID + "\t" + this.getStartTime() + "\t" + this.programs.get(i).name + "\n");
			}
		}
		return str;
	}
	
	/**
	 * Tests whether this process dispatcher contains specific program.
	 * @param id		ID of the program.
	 * @return			True if this process dispatcher contains specific program, false otherwise.
	 */
	public AbstractProcess containsProgram(long id) {
		boolean running = false;
		for (int i = 0; i < this.programs.size(); i++) {
			long programID = this.programs.get(i).getPid();
			if (programID == this.actualPID.get()) {
				running = true;
			}
			if (running && id == programID) {
				return this.programs.get(i);
			}
		}
		return null;
	}

	public long getActualPID() {
		return actualPID.get();
	}
	
	private void setActualPID(long actID) {
		this.actualPID.set(actID);
	}
	
	public String getActualName() {
		return name.toString();
	}
	
	private void setActualName(String name) {
		this.name = new StringBuffer(name);
	}
	
	public long getDispId() {
		return id;
	}
	
	public String getUserId() {
		return this.programs.get(0).getUserID();
	}
	
	public List<AbstractProcess> getPrograms() {
		return this.programs;
	}
}
